<?php

/**
 * Copyright 2019 Huawei Technologies Co.,Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use
 * this file except in compliance with the License.  You may obtain a copy of the
 * License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed
 * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
 * CONDITIONS OF ANY KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations under the License.
 *
 */

namespace Obs\Internal\Signature;

use Obs\Internal\Common\Model;

class V4Signature extends AbstractSignature
{
    const CONTENT_SHA256 = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855';

    protected $region;

    protected $utcTimeZone;

    public function __construct($ak, $sk, $pathStyle, $endpoint, $region, $methodName, $signature, $utcTimeZone, $securityToken = false, $isCname = false)
    {
        parent::__construct($ak, $sk, $pathStyle, $endpoint, $methodName, $signature, $securityToken, $isCname);
        $this->region = $region;
        $this->utcTimeZone = $utcTimeZone;
    }

    public function doAuth(array &$requestConfig, array &$params, Model $model)
    {
        $result = $this->prepareAuth($requestConfig, $params, $model);

        $result['headers']['x-amz-content-sha256'] = self::CONTENT_SHA256;

        $bucketName = $result['dnsParam'];

        $result['headers']['Host'] = $result['host'];

        $time = null;
        if (array_key_exists('x-amz-date', $result['headers'])) {
            $time = $result['headers']['x-amz-date'];
        } elseif (array_key_exists('X-Amz-Date', $result['headers'])) {
            $time = $result['headers']['X-Amz-Date'];
        } else {
            // nothing handle
        }
        $timestamp = $time ? date_create_from_format('Ymd\THis\Z', $time, $this->utcTimeZone)->getTimestamp()
        : time();

        $result['headers']['Date'] = gmdate('D, d M Y H:i:s \G\M\T', $timestamp);

        $longDate = gmdate('Ymd\THis\Z', $timestamp);
        $shortDate = substr($longDate, 0, 8);

        $credential = $this->getCredential($shortDate);

        $signedHeaders = $this->getSignedHeaders($result['headers']);

        $canonicalstring = $this->makeCanonicalstring($result['method'], $result['headers'], $result['pathArgs'], $bucketName, $result['uriParam'], $signedHeaders);

        $result['cannonicalRequest'] = $canonicalstring;

        $signature = $this->getSignature($canonicalstring, $longDate, $shortDate);

        $authorization = "AWS4-HMAC-SHA256 Credential={$credential},SignedHeaders={$signedHeaders},Signature={$signature}";

        $result['headers']['Authorization'] = $authorization;

        return $result;
    }

    public function getSignature($canonicalstring, $longDate, $shortDate)
    {
        $stringToSign = [];
        $stringToSign[] = 'AWS4-HMAC-SHA256';

        $stringToSign[] = "\n";

        $stringToSign[] = $longDate;

        $stringToSign[] = "\n";
        $stringToSign[] = $this->getScope($shortDate);
        $stringToSign[] = "\n";

        $stringToSign[] = hash('sha256', $canonicalstring);

        $dateKey = hash_hmac('sha256', $shortDate, 'AWS4' . $this->sk, true);
        $regionKey = hash_hmac('sha256', $this->region, $dateKey, true);
        $serviceKey = hash_hmac('sha256', 's3', $regionKey, true);
        $signingKey = hash_hmac('sha256', 'aws4_request', $serviceKey, true);
        return hash_hmac('sha256', implode('', $stringToSign), $signingKey);
    }

    public function getCanonicalQueryString($pathArgs)
    {
        $queryStr = '';

        ksort($pathArgs);
        $index = 0;
        foreach ($pathArgs as $key => $value) {
            $queryStr .= $key . '=' . $value;
            if ($index !== count($pathArgs) - 1) {
                $queryStr .= '&';
            }
            $index++;
        }
        return $queryStr;
    }

    public function getCanonicalHeaders($headers)
    {
        $headersResult = [];
        foreach ($headers as $key => $value) {
            $headersResult[strtolower($key)] = $value;
        }
        ksort($headersResult);

        $canonicalHeaderStr = '';

        foreach ($headersResult as $key => $value) {
            $value = is_array($value) ? implode(',', $value) : $value;
            $canonicalHeaderStr .= $key . ':' . $value;
            $canonicalHeaderStr .= "\n";
        }
        return $canonicalHeaderStr;
    }

    public function getCanonicalURI($bucketName, $objectKey)
    {
        $uri = '';
        if ($this->pathStyle && $bucketName) {
            $uri .= '/' . $bucketName;
        }

        if ($objectKey) {
            $uri .= '/' . $objectKey;
        }

        if ($uri === '') {
            $uri = '/';
        }
        return $uri;
    }

    public function makeCanonicalstring($method, $headers, $pathArgs, $bucketName, $objectKey, $signedHeaders = null, $payload = null)
    {
        $buffer = [];
        $buffer[] = $method;
        $buffer[] = "\n";
        $buffer[] = $this->getCanonicalURI($bucketName, $objectKey);
        $buffer[] = "\n";
        $buffer[] = $this->getCanonicalQueryString($pathArgs);
        $buffer[] = "\n";
        $buffer[] = $this->getCanonicalHeaders($headers);
        $buffer[] = "\n";
        $buffer[] = $signedHeaders ? $signedHeaders : $this->getSignedHeaders($headers);
        $buffer[] = "\n";
        $buffer[] = $payload ? strval($payload) : self::CONTENT_SHA256;

        return implode('', $buffer);
    }

    public function getSignedHeaders($headers)
    {
        $headersResult = [];

        foreach ($headers as $key => $value) {
            $headersResult[] = strtolower($key);
        }

        sort($headersResult);

        $signedHeaders = '';

        foreach ($headersResult as $key => $value) {
            $signedHeaders .= $value;
            if ($key !== count($headersResult) - 1) {
                $signedHeaders .= ';';
            }
        }
        return $signedHeaders;
    }

    public function getScope($shortDate)
    {
        return $shortDate . '/' . $this->region . '/s3/aws4_request';
    }

    public function getCredential($shortDate)
    {
        return $this->ak . '/' . $this->getScope($shortDate);
    }
}
